<?php
/* ***** BEGIN LICENSE BLOCK *****
 * Version: GPL 3.0
 * This file is part of Persephone.
 *
 * Persephone is free software: you can redistribute it and/or modify it under the 
 * terms of the GNU General Public License as published by the Free Software
 * Foundation, version 3 of the License.
 *
 * Persephone is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Persephone.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Copyright (C) 2008 eCircle AG
 * 
 * Contributors:
 *		edA-qa mort-ora-y <edA-qa@disemia.com>
 * ***** END LICENSE BLOCK ***** */

require_once dirname(__FILE__) . '/dbschema.inc';

/**
 * TODO: names are not checked for PHP safety and the moment.
 */
class DBSchema_PHPEmitter {

	var $sc;
	var $base;
	
	public function __construct( DBSchema $sc ) {
		$this->sc = $sc;
	}
	
	/**
	 * Emit the files needed for the PHP generated code
	 *
	 * @param base [in] where to write files
	 */
	public function emit( $base ) {
		$this->base = $base;
		$this->emitSchema( );
		
		foreach( $this->sc->forms as $form )
			$this->emitForm( $form );
			
		foreach( $this->sc->listings as $listing )
			$this->emitListing( $listing );
	}
	
	public function emitSchema( ) {
		$this->produceFile( 'schema', 'genSchema', null );
	}
	
	function emitForm( DBSchema_Form $form ) {	
		$this->produceFile( $form->name . '.form', 'genForm', array( $form ) );
	}
	
	function emitListing( DBSchema_Listing $listing ) {
		$this->produceFile( $listing->name . '.listing', 'genListing', array( $listing ) );
	}
	
	function produceFile( $name, $genFunc, $genArgs ) {
		$fullname = "{$this->base}{$name}.inc";
		$out = fopen( $fullname , 'w' );
		if( $out === false )
			die( "cannot open file: $fullname" );
			
		ob_start();
		
		print( "<?php\n/* This file was generated by " . __FILE__ . ". DO NOT EDIT THIS FILE! */\n?>\n" );
		
		$this->$genFunc( $genArgs );
		
		$data = ob_get_contents();
		ob_end_clean();
		fwrite( $out, $data);
		
		fclose( $out );
	}
	
	function genSchema( $ignore ) {
		print( "<?php\n" );
		$this->genBaseRequires();
		foreach( $this->sc->entities as &$entity ) {
			$this->genEntity( $entity );
		}
		print( "\n?>" );
	}
	
	function genBaseRequires() {
		print( "require_once 'persephone/entity_base.inc';\n" );
		print( "require_once 'persephone/query.inc';\n" );
	}
	
	function className( $str ) {
		return 
			strtoupper( substr( $str, 0, 1 ) )
			. substr( $str, 1 );
	}
	
	function instClassName( DBSchema_Entity $en ) {
		if( $en->class !== null )
			return $this->className( $en->class );
		return $this->className( $en->name );
	}
	
	function memberName( $str ) {
		//replace all first caps except last before non-cap
				
		//all caps  Ex: UPPER => upper
		if( preg_match( '/^\p{Lu}+$/', $str ) )
			return strtolower( $str );	
		
		//one leading cap  Ex: BasicName => basicName
		if( preg_match( '/^(\p{Lu})([^\p{Lu}].*)$/', $str, $m ) === 1 )
			return strtolower( $m[1] ) . $m[2];
			
		//many leading caps Ex: IDString => idString
		return preg_replace_callback( 
			'/^(\p{Lu}+)(\p{Lu}[^\p{Lu}])/', 
			create_function( '$matches', 'return strtolower( $matches[1] ) . $matches[2];' ),
			$str
			);
	}
	
	function constantExpr( DBSchema_Type $type, $value ) {
		switch( $type->name ) {
			case 'String':
			case 'Text':
				return '\'' . addslashes( $value ) . '\'';
				break;
			case 'Integer':
				return intval( $value );
			case 'Float':
			case 'Decimal':
				return floatval( $value ); 
			default:
				throw new Exception( "Constant expression not supported: {$type->name} for value: $value" );
		}
	}
	
	function args_or_array() { 
		//allow an array to be used instead of variable arguments
		return '$args = func_get_args();	
			if( count( $args ) === 1 && is_array( $args[0] ) )
				$args = $args[0];
			';
 	}
 	
	////////////////////////////////////////////////////////////////////////////
	function genEntity( DBSchema_Entity &$en ) {
		$this->genOpenEntityClass( $en );
		$this->genEntityDataTypes( $en );		
		
		foreach( $this->sc->mappers as $name => &$mapper ) { //TODO: need different format for multiple mappers
			if( $name != $en->name ) 
				continue;
				
			$this->genMapper( $en, $mapper );
			} 
			
		$this->genCloseEntityClass( $en );
	}
		
	////////////////////////////////////////////////////////////////////////////
	function genMapper( &$en, &$loc ) {
			
		$this->genConverters( $en, $loc );
		$this->genMaybeLoad( $en, $loc );
		$this->genAddSave( $en, $loc );
		$this->genSearch( $en, $loc );
		$this->genDelete( $en, $loc );
		$this->genEmpty( $en, $loc );
?>
static private function &getDB() {
	if( !isset( $GLOBALS['<?= $loc->provider->varname ?>'] ) )
		throw new ErrorException( "The database variable <?= $loc->provider->varname ?> is not defined." );
	return $GLOBALS['<?= $loc->provider->varname ?>'];
}
		
<?php
		$keyset = $en->getKeySet();
		foreach( $keyset as $keys ) {
			$keyName = '';
			$keyParamStr = '';
			for( $i =0 ; $i < count( $keys ); $i++ ) {
				if( $i ) {
					$keyName .= '_';
					$keyParamStr .= ', ';
				}
				$keyName .= $keys[$i]->name;
				$keyParamStr .= "\$key$i";
			}
			
			//Emit the finder to load from the DB (TODO: ensure only one record exists!)
?>
static public function &findWith<?=$keyName?>(<?= $keyParamStr ?>) {
	$ret =& self::with<?=$keyName?>(<?=$keyParamStr?>);
	
	if( !$ret->_maybeLoad() )
		throw new Exception( "Failed to find a record (<?=$keyName?>) / (<?= $keyParamStr ?>)" );
	$ret->_status = DBS_EntityBase::STATUS_EXTANT;
		
	return $ret;
}

static public function &findOrCreateWith<?=$keyName?>(<?= $keyParamStr?>) {
	$ret =& self::with<?=$keyName?>(<?=$keyParamStr?>);
	
	if( $ret->_maybeLoad() )
		$ret->_status = DBS_EntityBase::STATUS_EXTANT;
	else
		$ret->_status = DBS_EntityBase::STATUS_NEW;
	
	return $ret;
}


//create an object with the specified key (no other fields will be loaded until needed)
static public function &with<?=$keyName?>(<?=$keyParamStr?>) {
	$ret = new <?=$this->instClassName($en)?>();
<?php
	for( $i = 0; $i < count( $keys ); $i++ ) {
		print( "\t\$ret->{$this->memberName($keys[$i]->name)} = \$key$i;\n" );
	}
?>
	
	return $ret;
}

static public function &createWith<?=$keyName?>(<?=$keyParamStr?>) {
	$ret =& self::with<?=$keyName?>(<?=$keyParamStr?>);
	$ret->_status = DBS_EntityBase::STATUS_NEW;
	return $ret;
}

<?php
		} //end of for-loop for keyset
	}
	
	////////////////////////////////////////////////////////////////////////////
	function dbField( $field ) {
		return "array( '{$field->db_col}', '{$field->db_type->name}' )";
	}
	
	function dbFieldValue( $field, $member ) {
		return "array( '{$field->db_col}', '{$field->db_type->name}', \$this->{$this->memberName($member)} )";
	}
	
	////////////////////////////////////////////////////////////////////////////
	function genSearch( &$en, &$loc ) { ?>
/**
 * Obtains an iterable form of the results. They are to be loaded only on demand...
 * This accepts a variable number of parameters for the query options/conditions.
 */
static public function search( ) {
	<?= $this->args_or_array(); ?>
	
	return dbs_dbsource_search( 
		self::getDB(),
		'<?=$loc->table?>',
		'_<?=$this->instClassName($en)?>_privConstruct',
		array(
			<?php
				foreach( $loc->fields as $field )
					print( "'{$this->memberName($field->ent_field->name)}' => " . $this->dbField( $field ) . ",\n" );
			?>
		),
		$args	//pass all options to loader
		);
}
<?php
	}
	
	////////////////////////////////////////////////////////////////////////////
	function genDelete( &$en, &$loc ) { ?>
/**
 * Deletes all items matching the provided query.
 */
static public function searchAndDelete( ) {
	<?= $this->args_or_array() ?>
	return self::_searchAndDelete( $args );
}

static private function _searchAndDelete( $args ) {

	return dbs_dbsource_delete( 
		self::getDB(),
		'<?=$loc->table?>',
		'_<?=$this->instClassName($en)?>_privConstruct',
		array(
			<?php
				foreach( $loc->fields as $field )
					print( "'{$this->memberName($field->ent_field->name)}' => " . $this->dbField( $field ) . ",\n" );
			?>
		),
		$args	//pass all options to deleter
		);
}

//TODO: as with everything, support multiple locators
public function delete() {
	$query = array();
	$query[] = DBS_Query::limit( 1 );	//a safety measure
	$keys = array();
<?php
	//only need one ALT_RECORD_KEY, but need all RECORD_KEY
	foreach( $loc->fields as $field ) {
		$key = $field->ent_field;
		if( $key->keyType == DBSchema_Entity_Field::KEY_TYPE_NONE )
			continue;
		if( $key->keyType == DBSchema_Entity_Field::KEY_TYPE_RECORD ) {
			print( "\tif( \$this->__has('{$this->memberName($key->name)}') ) \n" );
			print( "\t\t\$keys[] = DBS_Query::match( '{$this->memberName($key->name)}', \$this->{$this->memberName($key->name)} );\n" );
			print( "\telse throw new DBS_FieldException( '{$this->memberName($key->name)}', DBS_FieldException::MISSING_REQ );\n" );
		} else if( $key->keyType == DBSchema_Entity_Field::KEY_TYPE_ALT ) {
			print( "\tif( \$this->__has('{$this->memberName($key->name)}') ) \n" );
			print( "\t\t\$keys[] = DBS_Query::match( '{$this->memberName($key->name)}', \$this->{$this->memberName($key->name)} );\n" );
		} else {
			throw new Exception( "Unsupported key type in entity for delete: {$key->name}" );
		}
			
	}
?>
	if( count( $keys ) == 0 )
		throw new Exception( "No key specified for delete" );
		
	$query[] = count( $keys ) > 1 ? DBS_Query::matchAndGroup( $keys ) : $keys[0];
	
	$this->_searchAndDelete( $query );
		
	$this->_status = DBS_EntityBase::STATUS_DELETED;
}
<?php
	}
	
	////////////////////////////////////////////////////////////////////////////
	function genOpenEntityClass( &$en ) {
?>
class <?= $this->className( $en->name ) ?> extends DBS_EntityBase {
	protected function __construct() {
		parent::__construct();
	} 
	
	static public function &_privConstruct() {
		$item = new <?= $this->instClassName($en) ?>();
		return $item;
	}
	
<?php
	}
	
	////////////////////////////////////////////////////////////////////////////
	function genCloseEntityClass( &$en ) { ?>
} //end of class

function &_<?= $this->instClassName($en) ?>_privConstruct() {
	return <?= $this->instClassName($en) ?>::_privConstruct();
}
		<?php
	}
	
	////////////////////////////////////////////////////////////////////////////
	function genEntityDataTypes( &$en ) { ?>
	protected $_data_names = array(
		<?php	
			foreach( $en->fields as $field ) {
				print( "'{$this->memberName( $field->name )}' => array(" );
				print( $field->type->baseType() ? 'null, ' :  "'{$field->type->name}'," );
				print( "'{$field->type->getRootType()->name}'," );
				print( "),\n" );
			}
		?>
		);
	
	protected $_data_defaults = array(
		<?php
			foreach( $en->fields as $field ) {
				if( !$field->hasDefault )
					continue;
				print( "'{$this->memberName( $field->name )}' => " . $this->constantExpr( $field->type, $field->defaultValue ) . ",\n" );
			}
		?>
		);
		
	protected $_data_aliases = array(
		<?php
			foreach( $en->aliases as  $alias => $field ) {
				print( "'{$this->memberName( $alias )}' => '{$this->memberName( $field )}'\n"  );
			}
		?>
		);
		
	protected function _checkType( $field, $value ) {
		switch( $field ) {
		<?php
			//TODO: somehow I'm guessing a static array of check functions would likely be more efficient...?
			foreach( $en->fields as $field ) {
				$name = "'{$this->memberName( $field->name )}'";
				print( "case $name:\n" );
				print( "if( \$value === null )" );
				if( $field->allowNull )
					print( "break;\n" );
				else
					print( "throw new DBS_SetFieldException( $name, DBS_SetFieldException::TYPE_NULL );\n" );
				switch( $field->type->name ) {
					case 'Integer':
					case 'Float':
					case 'Decimal':
						print( "if( !is_numeric( \$value ) ) 
							throw new DBS_SetFieldException( $name, DBS_SetFieldException::TYPE_NUMERIC );
							" );
						break;
						
					case 'String':
					case 'Text':
						if( $field->maxLen !== null )
							print( "if( strlen( \$value ) > $field->maxLen ) 
								throw new DBS_SetFieldException( $name, DBS_SetFieldException::TYPE_LEN );
								" );
						break;
				}
				print( "\tbreak;\n" );
			}
		?>
		}
	}
		
	<?php
	}

	////////////////////////////////////////////////////////////////////////////
	function genAddSave( &$en, &$loc ) {
		$keys = $en->getRecordKeyFields();
?>
	
protected function _save() {

	$keys = array();
<?php
	//TODO: basically a duplicate chunk from in maybeLoad
	//TODO: only need one ALT_RECORD_KEY, support READONLY keys
	for( $i = 0; $i < count($keys); $i++ ) {
		print( "\tif( \$this->__has('{$this->memberName($keys[$i]->name)}') ) \n" );
		print( "\t\t\$keys['{$this->memberName($keys[$i]->name)}'] = " 
			. $this->dbFieldValue( $loc->getDBFieldForEntityField( $keys[$i] ), $keys[$i]->name ) . ";\n" );
	}
?>

	//use the same keys as loading to allow for modifying key fields
	if( $this->_load_keys !== null )
		$usekeys = $this->_load_keys;
	else
		$usekeys = $keys;
<?php
	
	//check that no read-only fields have been modified
	for( $i = 0; $i < count( $keys ); $i++ ) {
		if( !$keys[$i]->loadOnly )
			continue;
		print( "\tif( \$this->__isDirty('{$this->memberName($keys[$i]->name)}') )\n" );
		print( "\t\tthrow new DBS_FieldException( '{$this->memberName($keys[$i]->name)}', DBS_FieldException::SAVE_LOAD_ONLY );\n" );
	}
?>
	
	dbs_dbsource_save( 
		self::getDB(),
		'<?=$loc->table?>',
		$this,
		$usekeys,
		array(
		<?php
			foreach( $loc->fields as $field) {
				if( $field->ent_field->loadOnly )	//for safety just don't save such fields
					continue;
				print( "'{$this->memberName($field->ent_field->name)}' => " . $this->dbField( $field ) . ",\n" );
			}
		?>
		),
		<?php
			if( $loc->insertField === null )
				print( "null" );
			else
				print( "array( '{$this->memberName($loc->insertField->ent_field->name)}' => " . $this->dbField( $loc->insertField ) . ")" );
		?>
		);
	$this->_status = DBS_EntityBase::STATUS_EXTANT;
	//now these are the keys which identify this object
	$this->_load_keys = $keys;
}

<?php
	}
	
	////////////////////////////////////////////////////////////////////////////
	function genMaybeLoad( &$en, &$loc ) {
		$keys = $en->getRecordKeyFields();
		?>
	
//public only for helpers (so search can indicate item was loaded)
public function  _set_load_keys() {
	if( $this->_load_keys !== null )	
		throw new Exception( "Not expecting a duplicate load / not supported" );
		
	$this->_load_keys = array();
<?php
	//TODO: only need one ALT_RECORD_KEY, support READONLY keys
	for( $i = 0; $i < count($keys); $i++ ) {
		$key = $keys[$i];
		if( $key->keyType == DBSchema_Entity_Field::KEY_TYPE_RECORD ) {
			print( "\tif( \$this->__has('{$this->memberName($key->name)}') ) \n" );
			print( "\t\t\$this->_load_keys['{$this->memberName($key->name)}'] = " 
				. $this->dbFieldValue( $loc->getDBFieldForEntityField( $key ), $key->name ) . ";\n" );
			print( "\telse throw new DBS_FieldException( '{$this->memberName($key->name)}', DBS_FieldException::MISSING_REQ );\n" );
		} else if( $key->keyType == DBSchema_Entity_Field::KEY_TYPE_ALT ) {
			print( "\tif( \$this->__has('{$this->memberName($key->name)}') ) \n" );
			print( "\t\t\$this->_load_keys['{$this->memberName($key->name)}'] = " 
				. $this->dbFieldValue( $loc->getDBFieldForEntityField( $key ), $key->name ) . ";\n" );
		} else {
			throw new Exception( "Unsupported key type in entity for load: {$key->name}" );
		}
	}
?>

	//return true if keys are complete, false otherwise
	return count( $this->_load_keys ) > 0;
}

protected function _maybeLoad() {
	$this->_set_load_keys();
		
	if( count( $this->_load_keys ) == 0 )
		throw new Exception( "No keys specified/set for loading" );
		
	if( !dbs_dbsource_load( 
		self::getDB(),
		'<?=$loc->table?>',
		$this, 
		$this->_load_keys,
		array(
		<?php
			foreach( $loc->fields as $field )
				print( "'{$this->memberName($field->ent_field->name)}' => " . $this->dbField( $field ) . ",\n" );
		?>
		) 
		) ) {
		$this->_load_keys = null;	//reset these keys as we didn't actually load
		return false;
	}
		
	return true;
}

<?php
	}
	
	/////////////////////////////////////////////////////////////////////////////
	function genConverters( &$en, &$loc ) {
		foreach( $loc->fields as $field ) {
			//// DB => Member
			print( "public static function _cnv_F{$loc->table}_{$field->db_col}_T{$field->ent_field->name}( \$value ) {\n" );
			$src_type = $field->db_type;
			
			if( $field->db_convert_func !== null ) {
				print( "\$value = {$field->db_convert_func}( \$value );" );
				$src_type = $field->db_convert_type;
			}
			
			if( $field->ent_field_field !== null ) {
				if( $src_type->name !== $field->ent_field_field->type->name )
					print( "\$value = convert_{$src_type->name}_to_{$field->ent_field_field->type->name}( \$value );\n" );
					
				print( "\$value = " . $this->className($field->ent_field->type->name) . 
					"::with{$field->ent_field_field->name}( \$value );\n" );
			} else if( $src_type->name !== $field->ent_field->type->name ) {	
				print( "\$value = convert_{$src_type->name}_to_{$field->ent_field->type->name}( \$value );\n" );
			}
			print( "return \$value;\n" );
			print( "}\n" );
			
			
			//// Member => DB
			print( "public static function _cnv_F{$field->ent_field->name}_T{$loc->table}_{$field->db_col}( \$value ) {\n" );
			$tar_type = $field->db_convert_func !== null ? $field->db_convert_type : $field->db_type;
			if( $field->ent_field_field !== null ) {
				print( "\$value = \$value->" . $this->memberName($field->ent_field_field->name) . ";\n" );
				if( $tar_type->name !== $field->ent_field_field->type->name )
					print( "\$value = convert_{$field->ent_field_field->type->name}_to_{$tar_type->name}( \$value );\n" );
			} else if( $tar_type->name !== $field->ent_field->type->name ) {
				print( "\$value = convert_{$field->ent_field->type->name}_to_{$tar_type->name}( \$value );\n" );
			}
			
			if( $field->db_convert_func !== null ) {
				print( "\$value = {$field->db_convert_func}_inv( \$value );" );
			}
			
			print( "return \$value;\n" );
			print( "}\n\n" );
		}
	}
	
	/////////////////////////////////////////////////////////////////////////////
	function genEmpty( &$en, &$loc ) { ?>
static public function &withNothing() {
	$ret = new <?=$this->instClassName($en)?>();
	return $ret;
}

static public function &createWithNothing() {
	$ret = new <?=$this->instClassName($en)?>();
	$ret->_status = DBS_EntityBase::STATUS_NEW;
	return $ret;
}
<?php
	}
	
	////////////////////////////////////////////////////////////////////////////
	
	/***************************************************************************
	 * Form Generation
	 ***************************************************************************/	
	function genForm( $data ) {
		list( $form ) = $data;
		
		print( "<?php\n" );
		?>
		
	require_once dirname(__FILE__).'/schema.inc';
	require_once 'persephone/form_base.inc';
					
	class Form<?=$this->className($form->name)?> extends DBS_FormBase_QuickForm {
	
		const ENTITY = '<?=$this->className($form->entity->name)?>';
		
		private $createFrom;
		
		protected function __construct( &$form ) {
			parent::__construct($form);
		}
		
		static public function &fromRequest() {
			$ret =& self::_setup();
			$ret->isNew = get_request_bool( DBS_FormBase_QuickForm::T_MARKNEW );
			return $ret;
		}
		
		//
		static public function &create( $from = null ) {
			$ret =& self::_setup();
			$ret->createFrom = $from;
			$ret->isNew = $from === null || $from->isNew();
			return $ret;
		}
		
		static private function &_setup( ) {
			$form = new HTML_QuickForm( '<?=$this->className($form->name)?>', 'POST', '', '', 
				array( 'class' => 'dbsform' ) );
<?php
			foreach( $form->fields as $name => $formfield ) {
				$field = $form->entity->fields[$name];
				
				if( $formfield->readonly )
					print( "\t\$form->addElement( 'static', '_ro_{$this->formNameOf( $field )}', {$this->formLabelOf( $field )} );\n" );
					
				//readonly also need to propagate their value, perhaps just for keys?
				if( $formfield->hidden || $formfield->readonly )
					print( "\t\$form->addElement( 'hidden', '{$this->formNameOf( $field )}' );\n" );
				else {
					print( "\t\$form->addElement( '{$this->formTypeOf( $field->type )}',"
						. " '{$this->formNameOf( $field )}', {$this->formLabelOf( $field )}, "
						. " {$this->formOptionsOf( $field )} );\n" );
					if( $field->maxLen !== null )
						print( "\t\$form->addRule( '{$this->formNameOf( $field )}', 
							{$this->formLabelOf( $field )} . ' may not be longer than {$field->maxLen} characters.', 'maxlength', {$field->maxLen}, 'client' );\n" );
							
					//TODO: isNumeric function, but where?
					if( $field->type->name === 'Integer' 
						|| $field->type->name === 'Decimal'
						|| $field->type->name === 'Float' )
						print( "\t\$form->addRule( '{$this->formNameOf( $field )}', 
							{$this->formLabelOf( $field )} . ' must be numeric.', 'numeric', true, 'client' );\n" );
				}
			}
?>
			$ret = new Form<?=$this->className($form->name)?>( $form );
			return $ret;
		}
		
		protected function addActions() {
			if( $this->isNew )
				$submit[] =& $this->form->createElement( 'submit', DBS_FormBase_QuickForm::T_ACTION_ADD, 'Add' );
			else {
				$submit[] =& $this->form->createElement( 'submit', DBS_FormBase_QuickForm::T_ACTION_SAVE, 'Save' );
			<?php if( $form->allowDelete ) { ?>
				$submit[] =& $this->form->createElement( 'submit', DBS_FormBase_QuickForm::T_ACTION_DELETE, 'Delete' );
			<?php } ?>
			}
			$this->form->addGroup( $submit, DBS_FormBase_QuickForm::T_SUBMITROW );
		}
		
		public function inject( &$entity, $overrideRequest = false ) {
			$values = array();
<?php
			foreach( $form->fields as $name => $formfield ) {
				$field = $form->entity->fields[$name];
				
				//only inject those values set on the object, this requires a forced (TODO: what about lazy loading... perhaps only if status is not EXTANT )
				print( "if( \$entity->__has( '{$this->memberName($formfield->name)}' ) ) {\n" );
				print( "\$values['{$this->formNameOf( $field )}'] = "
					. " {$this->formInFunc($field,$formfield)};\n " );
				if( $formfield->readonly )	//set above in _setup
					print( "\$values['_ro_{$this->formNameOf( $field )}'] = "
						. " {$this->formInFunc($field,$formfield)};\n " );
				
				print( "}\n" );
			}
?>
			if( $overrideRequest )
				$this->form->setConstants( $values );
			else
				$this->form->setDefaults( $values );
		}
	
		public function extractKeys( &$entity ) {
			<?php $this->formExtract( $form, true ); ?>
		}
		
		public function extract( &$entity ) {
			<?php $this->formExtract( $form, false ); ?>
		}
	
		public function execute() {
			if( !$this->hasAction() ) {
				if( $this->createFrom !== null )
					$this->inject( $this->createFrom );
			}
				
			$showForm = true;
			if( $this->validate() ) {
				if( $this->isNew ) {
					$rule = <?=$this->className($form->entity->name)?>::createWithNothing();
				} else {
					$rule = <?=$this->className($form->entity->name)?>::withNothing();
					$this->extractKeys( $rule );
					$rule->find();
				}
				
				$this->extract( $rule );
				if( $this->getAction() == DBS_FormBase::ACTION_SAVE ) {
					$rule->save();
					$this->inject( $rule, true );	//capture any logic/new values from entity
					print( "<p class='success'>Saved.</p>" );
				} else if( $this->getAction() == DBS_FormBase::ACTION_ADD ) {
					$rule->add();
					$this->isNew = false;
					$this->inject( $rule, true );	//capture any logic/new values from entity
					print( "<p class='success'>Added.</p>" );
				} else if( $this->getAction() == DBS_FormBase::ACTION_DELETE ) {
					$rule->delete();
					$showForm = false;
					print( "<p class='success'>Deleted.</p>" );
				}
			}
			
			if( $showForm ) {
				$this->addActions();
				echo $this->toHTML();
			}
		}
	
	}
<?php
		print( "?>" );
	}
	
	function formExtract( $form, $keys ) {
		foreach( $form->fields as $name => $formfield ) {
			$field = $form->entity->fields[$name];
			
			//skip keys in key mode, or only keys otherwise
			if( ($field->keyType != DBSchema_Entity_Field::KEY_TYPE_NONE) != $keys)
				continue;
				
			//TODO: what does readonly mean here...?
			//if( $formfield->readonly ) 
			//	continue;
				
			//TODO: references for entitites
			print( "\$raw = \$this->form->exportValue('{$this->formNameOf( $field )}');\n" );
			print( $this->formOutFunc( $field, $formfield ) );
		}
	}
		
	function formLinkFieldOf( DBSchema_Entity $ent ) {
		$keys = $ent->getRecordKeyFields();
		if( count( $ent ) != 1 )
			$this->error( "Type {$ent->name} has too many keys" );
			
		return $keys[0];
	}
	
	function formTypeOf( DBSchema_Type $type ) {
		$type = $type->getRootType();
		if( $type instanceof DBSchema_Entity ) {
			$link = $this->formLinkFieldOf( $type );
			if( $type->getTitle() !== null )
				return 'select';
			$type = $link->type;
		}
		
		switch( $type->name ) {
			 case 'String':
			 case 'Integer':
			 case 'Decimal':
			 	return 'text';
			 case 'Text':
			 	return 'textarea';
			 case 'Bool';
			 	return 'select';
			 default:
			 	throw new Exception( "Unsupported Form type: {$type->name}" );
		}
	}
	
	function formNameOf( DBSchema_Entity_Field $ent ) {
		return "_dbs_{$ent->name}";	//TODO: proper naming
	}
	
	function formLabelOf( DBSchema_Entity_Field $ent ) {
		return '\'' . xml( $ent->name ) . '\'';	//TODO:FEATURE: some label lookup/replacement
	}
	
	function formOptionsOf( DBSchema_Entity_Field $ent ) {
		if( $ent->type->getRootType()->name == 'Bool' )
			return "array( 0 => 'False', 1 => 'True' )";
			
		if( $ent->type instanceof DBSchema_Entity ) {
			$link = $this->formLinkFieldOf( $ent->type );
			$title = $ent->type->getTitle();
			if( $title !== null ) {
				//just match all records by default
				return " _dbs_form_loadentityselect( "
					. "{$this->className($ent->type->name)}::search( DBS_Query::matchAll() ), "
					. "'{$this->memberName($link->name)}',"
					. "'{$this->memberName($title->name)}' )"
					;
			}
		}			
		
		if( $ent->type->name === 'String' && $ent->maxLen !== null )
			return "array( 'maxlength' => {$ent->maxLen} )";
			
		return 'array()';
	}
	
	function formInFunc( DBSchema_Entity_Field $ent, DBSchema_Form_Field $ff ) {
		if( $ent->type instanceof DBSchema_Entity ) {
			$link = $this->formLinkFieldOf( $ent->type );
			$type = $link->type;
			$sub = '->' . $this->memberName( $link->name );
		} else {
			$type = $ent->type->getRootType();
			$sub = '';
		}
				
		return "_dbs_formin_{$type->name}"
			. "( \$entity->{$this->memberName($ff->name)}$sub )";
	}
	
	function formOutFunc( DBSchema_Entity_Field $ent, DBSchema_Form_Field $ff ) {
		if( $ent->type instanceof DBSchema_Entity ) {
			$link = $this->formLinkFieldOf( $ent->type );
			$type = $link->type;
			$sub = 'unset($ent); $ent =& ' . $this->className($ent->type->name) . "::withNothing();\n";
			$sub .= "\$ent->{$this->memberName($link->name)} = \$raw;\n";
			$assign = '= new DBS_Ref( $ent )';
		} else {
			$type = $ent->type->getRootType();
			$assign = '= $raw';
			$sub = '';
		}
		
		$buf = "\$raw = _dbs_formout_{$type->name}(\$raw);\n";
		$buf .= $sub;
		$buf .= "\$entity->{$this->memberName($ff->name)} $assign;\n";
		return $buf;
	}
	
	////////////////////////////////////////////////////////////////////////////
	
	/***************************************************************************
	 * Form Generation
	 ***************************************************************************/	
	function genListing( $data ) {
		list( $listing ) = $data;
		print( "<?php\n" );
		?>
		
	require_once dirname(__FILE__).'/schema.inc';
	require_once 'persephone/listing_base.inc';
					
	class Listing<?=$this->className($listing->name)?> extends DBS_ListingBase {
	
		protected $entity = '<?=$this->className($listing->entity->name)?>';
	
		protected function __construct( $searchArgs ) {
			parent::__construct( $searchArgs );
		}
		
		static public function search( ) {
			<?= $this->args_or_array() ?>
			return new Listing<?=$this->className($listing->name)?>( $args );
		}
	<?php
		print( "protected \$fields = array(\n" );
		foreach( $listing->fields as $field ) {
			if( $field->entField === null )
				$membername = '@SELF';
			else
				$membername = $this->memberName($field->entField->name);
			
			if( $field->convertFunc !== null ) 
				$converter = $field->convertFunc;
			else {
				//TODO: support fallback through base type chain for custom types
				$converter = "format_listing_{$field->entField->type->name}";
			}
			
			print( "\tarray( '$membername', " 
				. '"' . addslashes( $field->label ) //label
				. "\",'$converter'" //converter
				. "),\n"
				);
		}
		print( ");\n" );
		print( "}\n" );
	}	
}

?>